﻿#region File and License Information
/*
<File>
	<License Type="BSD">
		Copyright © 2009 - 2012, Outcoder. All rights reserved.
	
		This file is part of Calcium (http://calciumsdk.net).

		Redistribution and use in source and binary forms, with or without
		modification, are permitted provided that the following conditions are met:
			* Redistributions of source code must retain the above copyright
			  notice, this list of conditions and the following disclaimer.
			* Redistributions in binary form must reproduce the above copyright
			  notice, this list of conditions and the following disclaimer in the
			  documentation and/or other materials provided with the distribution.
			* Neither the name of the <organization> nor the
			  names of its contributors may be used to endorse or promote products
			  derived from this software without specific prior written permission.

		THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
		ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
		WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
		DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
		DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
		(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
		LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
		ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
		(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
		SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	</License>
	<Owner Name="Daniel Vaughan" Email="danielvaughan@outcoder.com"/>
	<CreationDate>2010-10-21 15:34:42Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Input;

using Outcoder.CommandModel;
using Outcoder.ComponentModel;

namespace Outcoder.UI.Xaml
{
	/// <summary>
	/// Interface specifying the functionality to be implemented 
	/// by an object that wishe to manage the handling of exceptions.
	/// </summary>
	public interface IExceptionHandler
	{
		bool HandleException(Exception exception, object owner, string memberName = null, string fileName = null, int lineNumber = 0);
	}

	public interface IDelegateCommand
	{
		void RaiseCanExecuteChanged();

		Task RefreshAsync(object commandParameter);
	}

	public class DelegateCommand<TParameter> : IEventCommand, IDelegateCommand, ISupportInitialize, INotifyPropertyChanged
	{
		IExceptionHandler exceptionHandler;
		volatile bool ignoreNextCanExecuteCall;

#if WINDOWS_UWP
		bool parameterPrimitive = typeof(TParameter).GetTypeInfo().IsPrimitive;
#else
		bool parameterPrimitive = typeof(TParameter).IsPrimitive;
#endif

#if WINDOWS_PHONE
		string eventName = "Tap";
#else
		string eventName = "Click";
#endif

		public string EventName
		{
			get
			{
				return eventName;
			}
			set
			{
				eventName = value;
			}
		}

		readonly Action<TParameter> executeAction;
		readonly Func<TParameter, bool> canExecuteFunc;
	
		readonly Func<TParameter, Task> executeActionAsync;
		Func<TParameter, Task<bool>> canExecuteAsyncFunc;

		string filePath;
		int lineNumber;

		/// <summary>
		/// Creates an awaitable asynchronous command. 
		/// </summary>
		/// <param name="executeAction">The action to execute when the command is performed.</param>
		/// <param name="canExecuteFunc">A func that determines if the command 
		/// may be performed. Can be <c>null</c>.</param>
		/// <param name="exceptionHandler">When an exception occurs during execution or during evaluating 
		/// if the command can execute, then the exception is passed to the exception manager. 
		/// Can be <c>null</c>.</param>
		/// <param name="filePath">The path to the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		/// <param name="lineNumber">The line number of the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		public DelegateCommand(Action<TParameter> executeAction, Func<TParameter, bool> canExecuteFunc = null,
			IExceptionHandler exceptionHandler = null, 
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
		{
			this.executeAction = ArgumentValidator.AssertNotNull(executeAction, "executeFunc");
			this.canExecuteFunc = canExecuteFunc;
			this.filePath = filePath;
			this.lineNumber = lineNumber;

			SetExceptionHandler(exceptionHandler);
		}

		/// <summary>
		/// Creates an awaitable asynchronous command. 
		/// </summary>
		/// <param name="executeFunc">The func to execute when the command is performed.</param>
		/// <param name="asyncConstructorDisambiguator">Can be any value and is used 
		/// to disambiguate between this and the synchronous constructor.</param>
		/// <param name="canExecuteFunc">A func that determines if the command 
		/// may be performed. Can be <c>null</c>.</param>
		/// <param name="exceptionHandler">When an exception occurs during execution or during evaluating 
		/// if the command can execute, then the exception is passed to the exception manager. 
		/// Can be <c>null</c>.</param>
		/// <param name="filePath">The path to the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		/// <param name="lineNumber">The line number of the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		public DelegateCommand(
			Func<TParameter, Task> executeFunc,
			bool asyncConstructorDisambiguator,
			Func<TParameter, Task<bool>> canExecuteFunc = null, 
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
		{
			executeActionAsync = ArgumentValidator.AssertNotNull(executeFunc, "executeFunc");

			canExecuteAsyncFunc = canExecuteFunc;
			isAsync = true;

			this.filePath = filePath;
			this.lineNumber = lineNumber;

			SetExceptionHandler(exceptionHandler);
		}

		bool isAsync;

		protected bool IsAsync => isAsync;

		void SetExceptionHandler(IExceptionHandler exceptionHandler)
		{
			if (exceptionHandler != null)
			{
				this.exceptionHandler = exceptionHandler;
			}
			else
			{
				if (Dependency.Initialized)
				{
					Dependency.TryResolve<IExceptionHandler>(out this.exceptionHandler);
				}
			}
		}

		protected bool HandleException(
			Exception exception, 
			object owner, 
			[CallerMemberName]string memberName = null)
		{
			if (exceptionHandler != null)
			{
				return exceptionHandler.HandleException(exception, owner, memberName);
			}

			return false;
		}

		public Func<TParameter, Task<bool>> CanExecuteFunc
		{
			get
			{
				return canExecuteAsyncFunc;
			}
			set
			{
				if (canExecuteAsyncFunc != value)
				{
					canExecuteAsyncFunc = value;

					isAsync = canExecuteFunc != null;

					OnPropertyChanged();

					OnCanExecuteChanged(EventArgs.Empty);
				}
			}
		}

#region ICommand Members

#if SILVERLIGHT || MONODROID || __IOS__ || NETFX_CORE
		public event EventHandler CanExecuteChanged;
#else
		public event EventHandler CanExecuteChanged
		{
			add
			{
				if (canExecuteFunc != null)
				{
					System.Windows.Input.CommandManager.RequerySuggested += value;
				}
			}
			remove
			{
				if (canExecuteFunc != null)
				{
					System.Windows.Input.CommandManager.RequerySuggested -= value;
				}
			}
		}
#endif

		protected TParameter ProcessParameter(object parameter)
		{
			TParameter result;
			if (parameter == null)
			{
				result = DefaultParameter;
			}
			else
			{
				result = (TParameter)CoerceParameterToType(parameter);

				if (!parameterPrimitive && EqualityComparer<TParameter>.Default.Equals(DefaultParameter, default(TParameter)))
				{
					DefaultParameter = result;
				}
			}

			return result;
		}

		protected TParameter ProcessParameter(TParameter parameter)
		{
			TParameter result;
			if (EqualityComparer<TParameter>.Default.Equals(parameter, default(TParameter)))
			{
				result = DefaultParameter;
			}
			else
			{
				result = parameter;

				if (!parameterPrimitive && EqualityComparer<TParameter>.Default.Equals(DefaultParameter, default(TParameter)))
				{
					DefaultParameter = parameter;
				}
			}
			
			return result;
		}

		public bool CanExecute(object parameter = null)
		{
			TParameter coercedParameter = ProcessParameter(parameter);
			
			bool result;

			if (!isAsync)
			{
				try
				{
					result = CanExecute(coercedParameter);
				}
				catch (Exception ex)
				{
					result = false;

					if (exceptionHandler == null
						|| !exceptionHandler.HandleException(ex, this, "CanExecute", filePath, lineNumber))
					{
						throw;
					}
				}
			}
			else
			{
				RefreshCanExecute(coercedParameter);

				result = Enabled;
			}

			return result;
		}

		async void RefreshCanExecute(TParameter parameter)
		{
			try
			{
				await RefreshCanExecuteAsync(parameter);
			}
			catch (Exception ex)
			{
				exceptionHandler?.HandleException(ex, this);
			}
		}

		async Task RefreshCanExecuteAsync(TParameter parameter)
		{
			/* This method is intended to be called by the UI thread 
			 * as a result of a UI update. */
			if (ignoreNextCanExecuteCall)
			{
				ignoreNextCanExecuteCall = false;
				return;
			}
			
			if (canExecuteAsyncFunc != null)
			{
				bool temp = Enabled;

				try
				{
					temp = await canExecuteAsyncFunc(parameter);
				}
				catch (Exception ex)
				{
					if (exceptionHandler == null
					    || !exceptionHandler.HandleException(ex, this, "canExecuteAsyncFunc", filePath, lineNumber))
					{
						throw;
					}
				}

				if (Enabled != temp)
				{
					ignoreNextCanExecuteCall = true;

					Enabled = temp;

					OnCanExecuteChanged(EventArgs.Empty);
				}
			}
		}

		public virtual async Task<bool> CanExecuteAsync(TParameter parameter)
		{
			parameter = ProcessParameter(parameter);

			bool result;

			if (!isAsync)
			{
				result = CanExecute(parameter);
			}
			else
			{
				await RefreshCanExecuteAsync(parameter);

				result = Enabled;
			}

			return result;
		}

		void ICommand.Execute(object parameter)
		{
			TParameter coercedParameter = ProcessParameter(parameter);
			Execute(coercedParameter);
		}

		public virtual async void Execute(TParameter parameter = default(TParameter))
		{
			await ExecuteAsync(parameter);
		}

		public virtual async Task ExecuteAsync(TParameter parameter = default(TParameter))
		{
			try
			{
				parameter = ProcessParameter(parameter);

				if (!isAsync)
				{
					executeAction(parameter);
				}
				else
				{
					await executeActionAsync(parameter);
				}
			}
			catch (Exception ex)
			{
				if (exceptionHandler == null
					|| !exceptionHandler.HandleException(ex, this, "ExecuteAsync", filePath, lineNumber))
				{
					throw;
				}
			}
		}

		object CoerceParameterToType(object parameter)
		{
			object coercedParameter = parameter;
			Type typeOfT = typeof(TParameter);

#if NETFX_CORE
			if (parameter != null && !typeOfT.GetTypeInfo().IsAssignableFrom(parameter.GetType().GetTypeInfo()))
#else
			if (parameter != null && !typeOfT.IsAssignableFrom(parameter.GetType()))
#endif
			{
				coercedParameter = ImplicitTypeConverter.ConvertToType(parameter, typeOfT);
			}
			return coercedParameter;
		}

#endregion

		public virtual bool CanExecute(TParameter parameter = default(TParameter))
		{
			parameter = ProcessParameter(parameter);

			bool result;

			if (!isAsync)
			{
				if (canExecuteFunc == null)
				{
					result = true;
				}
				else
				{
					try
					{
						result = canExecuteFunc(parameter);
					}
					catch (Exception ex)
					{
						result = true;

						if (exceptionHandler == null
							|| !exceptionHandler.HandleException(ex, this, "CanExecute", filePath, lineNumber))
						{
							throw;
						}
					}
				}
				
				Enabled = result;
			}
			else
			{
#pragma warning disable 4014
				RefreshCanExecuteAsync(parameter);
#pragma warning restore 4014

				result = Enabled;
			}

			return result;
		}

		bool enabled = true;

		public bool Enabled
		{
			get
			{
				return enabled;
			}
			protected internal set
			{
				AssignPropertyValue(ref enabled, value, false);
			}
		}

#if SILVERLIGHT || MONODROID || __IOS__ || NETFX_CORE
		public void RaiseCanExecuteChanged()
		{
			OnCanExecuteChanged(EventArgs.Empty);
		}

		protected virtual void OnCanExecuteChanged(EventArgs e)
		{
			var tempEvent = CanExecuteChanged;
			if (tempEvent != null)
			{
				UISynchronizationContext.Instance.InvokeIfRequired(() => tempEvent(this, e));
			}
		}
#else
		public void RaiseCanExecuteChanged()
		{
			OnCanExecuteChanged(EventArgs.Empty);	
		}

		protected virtual void OnCanExecuteChanged(EventArgs e)
		{
			/* This is present for cross-platform compatibility. */
		}
#endif

		public event PropertyChangedEventHandler PropertyChanged;

		protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
		{
			var temp = PropertyChanged;
			if (temp != null)
			{
				temp.Invoke(this, new PropertyChangedEventArgs(propertyName));
			}
		}

		protected bool Initializing { get; set; }

		public void BeginInit()
		{
			Initializing = true;
		}

		public void EndInit()
		{
			Initializing = false;
		}

		public virtual async Task RefreshAsync(object commandParameter = null)
		{
			TParameter parameter = ProcessParameter(commandParameter);
			//var argOfT = ArgumentValidator.AssertOfType<TParameter>(commandParameter, "commandParameter");
#pragma warning disable 4014
			await RefreshCanExecuteAsync(parameter);
#pragma warning restore 4014

		}

		public virtual async Task RefreshAsync(TParameter parameter)
		{
			parameter = ProcessParameter(parameter);

			if (isAsync)
			{
				await RefreshCanExecuteAsync(parameter);
			}
			else
			{
				bool canCurrentlyExecute = Enabled;
				bool valueAfterUpdate = CanExecute(parameter);
				if (canCurrentlyExecute != valueAfterUpdate)
				{
					OnCanExecuteChanged(EventArgs.Empty);
				}
			}
		}

		protected void AssignPropertyValue<TField>(
			ref TField oldValue, 
			TField newValue, 
			bool raiseOnCanExecuteChanged,
			[CallerMemberName] string memberName = null)
		{
			if (!object.Equals(oldValue, newValue))
			{
				oldValue = newValue;

				if (!Initializing)
				{
					// ReSharper disable once ExplicitCallerInfoArgument
					OnPropertyChanged(memberName);

					if (raiseOnCanExecuteChanged)
					{
						OnCanExecuteChanged(EventArgs.Empty);
					}
				}
			}
		}

		public TParameter DefaultParameter { get; set; }
	}

	public class DelegateCommand : DelegateCommand<object>
	{
		/// <summary>
		/// Creates an awaitable asynchronous command. 
		/// </summary>
		/// <param name="executeAction">The action to execute when the command is performed.</param>
		/// <param name="canExecute">An action that determines if the command 
		/// may be performed. Can be <c>null</c>.</param>
		/// <param name="exceptionHandler">When an exception occurs during execution or during evaluating 
		/// if the command can execute, then the exception is passed to the exception manager. 
		/// Can be <c>null</c>.</param>
		/// <param name="filePath">The path to the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		/// <param name="lineNumber">The line number of the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		public DelegateCommand(
			Action<object> executeAction, 
			Func<object, bool> canExecute = null,
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
			: base(executeAction, canExecute, 
					exceptionHandler, filePath, lineNumber)
		{
			/* Intentionally left blank. */
		}

		/// <summary>
		/// Creates an awaitable asynchronous command. 
		/// </summary>
		/// <param name="executeFunc">The action to execute when the command is performed.</param>
		/// <param name="asyncConstructorDisambiguator">Can be any value and is used 
		/// to disambiguate between this and the synchronous constructor.</param>
		/// <param name="canExecuteFunc">A func that determines if the command 
		/// may be performed. Can be <c>null</c>.</param>
		/// <param name="exceptionHandler">When an exception occurs during execution or during evaluating 
		/// if the command can execute, then the exception is passed to the exception manager. 
		/// Can be <c>null</c>.</param>
		/// <param name="filePath">The path to the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		/// <param name="lineNumber">The line number of the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		public DelegateCommand(
			Func<object, Task> executeFunc,
			bool asyncConstructorDisambiguator,
			Func<object, Task<bool>> canExecuteFunc = null,
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
			: base(executeFunc, asyncConstructorDisambiguator, 
				canExecuteFunc, exceptionHandler, filePath, lineNumber)
		{
			/* Intentionally left blank. */
		}
	}
}
